//
// ppu.c
//
// CSE 420
// Project 4 : Part 3b
// Authors: Colin Hofman, Alan Tu, Adam Fairfield
//

// System includes
#include <stdio.h>
#include <stddef.h>
#include <stdint.h>
#include <ctype.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <libspe2.h>
#include <cbe_mfc.h>
#include <pthread.h>
#include <unistd.h>
#include <math.h>

// Local includes
#include "common.h"

// Data structure for running SPE thread
typedef struct ppu_pthread_data
{
	spe_context_ptr_t   speid;
	pthread_t           pthread;
	uint64_t						*argp;
} ppu_pthread_data_t;

// Global variables
int* matrixA_InPPE        QWORD_ALIGNED;							// Entire matrix a
int* matrixB_InPPE        QWORD_ALIGNED;							// Entire matrix b
int* matrixC_InPPE        QWORD_ALIGNED;							// Entire result matrix c
int a[BLOCK_SIZE_SQUARED] QWORD_ALIGNED;							// Single block of matrix a to feed to spu
int b[BLOCK_SIZE_SQUARED] QWORD_ALIGNED;							// Single block of matrix b to feed to spu
int c[BLOCK_SIZE_SQUARED] QWORD_ALIGNED;							// Single block of matrix c to feed to spu
extern spe_program_handle_t spu;//mult threads?											// Reference to spu program thread
CONTROL_BLOCK cb[NUM_THREADS] QWORD_ALIGNED;					// Control block structure to pass to spu
ppu_pthread_data_t data[NUM_THREADS];									// PPU program thread data

// Forward function declarations
void *ppu_pthread_function(void *arg);
void initMatrices();
void printMatrix(char *name, int *matrix);

//
// Main ppu function.
//
// @param   int   argc     Argument count
//          char* argv[]   Argument list
// @return  int            Return status
//
int main(int argc, char *argv[])
{
	int rc[NUM_THREADS];

	// Initialize A and B with random values
	initMatrices();
   
  // Print matrices A and B
	//printMatrix("A", matrixA_InPPE);
	//printMatrix("B", matrixB_InPPE);

#ifdef TIMING
		long unsigned int start_t, end_t;
		float total_t = 0;
#endif

    // Multiply matrices using block multiplication
    for (int i_block = 0; i_block < N; i_block+=(BLOCK_SIZE*NUM_THREADS))
    {
      for (int j_block = 0; j_block < N; j_block+=(BLOCK_SIZE*NUM_THREADS))
      {
				for (int k_block = 0; k_block < N; k_block+=(BLOCK_SIZE*NUM_THREADS))
				{
					for (int iteration = 0; iteration < ITERATIONS; iteration++)
					{
						for (int thread = 0; thread < NUM_THREADS; thread++)
						{
							// Pull out individual block chunks
							for (int i = 0; i < BLOCK_SIZE; ++i)	//Increment this differently? i+1+(BLOCK_SIZE*thread)?
							{
								 for (int j = 0; j < BLOCK_SIZE; ++j)
								 {
										for (int k = 0; k < BLOCK_SIZE; ++k)
										{
											 a[i * BLOCK_SIZE + k] = matrixA_InPPE[(i_block + i) * N + (k_block + k)];
											 b[k * BLOCK_SIZE + j] = matrixB_InPPE[(k_block + k) * N + (j_block + j)];
											 c[i * BLOCK_SIZE + j] = matrixC_InPPE[(i_block + i) * N + (j_block + j)];
										}
								 }
							}

							// Pack block data into control block
							cb[thread].blockA = &a;
							cb[thread].blockB = &b;
							cb[thread].SPU_result = &c;
							
							// Create SPE context and load SPE program into the SPE context
							if ((data[thread].speid = spe_context_create(thread, NULL)) == NULL)
							{
								fprintf(stderr, "Failed spe_context_create(errno=%d strerror=%s)\n", errno, strerror(errno));
								exit(3);
							}
			
							// Load program
							if ((rc[thread] = spe_program_load(data[thread].speid, &spu)) != 0)	//multi thread spu?
							{
								fprintf(stderr, "Failed spe_program_load(errno=%d strerror=%s)\n", errno, strerror(errno));
								exit(3);
							}

							// Pass Control Block address to SPU
							data[thread].argp = (uint64_t*)&cb[thread];
						}

#ifdef TIMING
	start_t = __mftb();
#endif

						for (int thread = 0; thread < NUM_THREADS; thread++)
						{
							// Create SPE pthreads
							if ((rc[thread] = pthread_create(&data[thread].pthread, NULL, &ppu_pthread_function, &data[thread])) != 0)
							{
								fprintf(stderr, "Failed pthread_create(errno=%d strerror=%s)\n", errno, strerror(errno));
								exit(3);
							}
						}
						
						for (int thread = 0; thread < NUM_THREADS; thread++)
						{
							// Wait for SPE to complete
							if ((rc[thread] = pthread_join(data[thread].pthread, NULL)) != 0)
							{
								fprintf(stderr, "Failed pthread_join(rc=%d, errno=%d strerror=%s)\n", rc[thread], errno, strerror(errno));
								exit(1);
							}
							
							// Store block that we computed back into the whole matrix
							for (int i = 0; i < BLOCK_SIZE; ++i) //i+1+(BLOCK_SIZE*thread)?
								 for (int j = 0; j < BLOCK_SIZE; ++j)
										for (int k = 0; k < BLOCK_SIZE; ++k)
											 matrixC_InPPE[(i_block + i) * N + (j_block + j)] = cb[thread].SPU_result[i * BLOCK_SIZE + j];	//Increment this differently? BLOCK_SIZE*thread
						}
						
#ifdef TIMING
	end_t = __mftb();
	total_t += (float)((end_t - start_t) / 79.8);
#endif

						for (int thread = 0; thread < NUM_THREADS; thread++)
						{
							// Destroy the SPE contexts
							if ((rc[thread] = spe_context_destroy(data[thread].speid)) != 0)
							{
								fprintf(stderr, "Failed spe_context_destroy(rc=%d, errno=%d strerror=%s)\n", rc[thread], errno, strerror(errno));
								exit(1);
							}
						}
					}
				}
      }
		}

#ifdef TIMING
  printf("\n\n\nTotal time used by SPE: %f us\n\n\n", total_t);
#endif

  // Print the result of multiplying AxB
	//printMatrix("C", matrixC_InPPE);

  // Free up dynamic memory
  free(matrixA_InPPE);
  free(matrixB_InPPE);
  free(matrixC_InPPE);

	return 0;
}

//
// Create and run an SPE thread
//
// @param   void* arg   Function pointer for spe thread
//
void *ppu_pthread_function(void *arg)
{
	ppu_pthread_data_t *datap = (ppu_pthread_data_t *)arg;
	int rc;
	unsigned int entry = SPE_DEFAULT_ENTRY;
	if ((rc = spe_context_run(datap->speid, &entry, 0, datap->argp, NULL, NULL)) < 0)
	{
		fprintf(stderr, "Failed spe_context_run(rc=%d, errno=%d, strerror=%s)\n", rc, errno, strerror(errno));
		exit(1);
	}
	pthread_exit(NULL);
}

// 
// Initialize matrices A and B with random integers from (0, 100]
// and matrix C to 0.
//
void initMatrices()
{
   // Allocate space
   matrixA_InPPE = (int*)malloc(MATRIX_SIZE * sizeof(int));
   matrixB_InPPE = (int*)malloc(MATRIX_SIZE * sizeof(int));
   matrixC_InPPE = (int*)malloc(MATRIX_SIZE * sizeof(int));

	for(int row = 0; row < N; row++)
	{
		for(int col = 0; col < N; col++)
		{
			matrixA_InPPE[row * N + col] = random() % 100;  // Random between (0, 100]
			matrixB_InPPE[row * N + col] = random() % 100;  // Random between (0, 100]
         matrixC_InPPE[row * N + col] = 0;
		}
	}
}

//
// Prints a matrix
//
// @param   char* name     Matrix name
//          int*  matrix   Matrix data
//
void printMatrix(char *name, int *matrix)
{
	printf("Matrix %s:\n", name);
	for(int row = 0; row < N; row++)
	{
		for(int col = 0; col < N; col++)
		{
			printf("%8d", matrix[row * N + col]);
		}
		printf("\n");
	}
}